package emulator.analyzer;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import emulator.EmulatorException;
import emulator.analyzer.util.CmpCommand;
import emulator.analyzer.util.CmpCommandMap;
import emulator.assembler.Assembler;
import emulator.assembler.Instruction;
import emulator.hardware.CPU;
import emulator.hardware.CPUExecutionObserver;
import emulator.hardware.HwWord;
import emulator.hardware.debug.BreakpointException;
import emulator.hardware.debug.BusWatchException;
import emulator.hardware.memory.MemoryBlockInterface;
import emulator.util.AddressRange;
import emulator.util.ByteHelper;

public class CpuExecutionAnalyzer implements CPUExecutionObserver {
	PrintStream log = System.out;
	Assembler assembler = new Assembler();
	MemoryBlockInterface memory;
	AddressRange code_range;
	HashMap<Integer, AddressDefinition> address_map = new HashMap<Integer, AddressDefinition>();
	HashMap<Character, Integer> reg_defines = new HashMap<Character, Integer>();
	CmpCommandMap cmp_map = new CmpCommandMap();
	int [] mem_defines;
	boolean allswell;
	private int last_pc = -1;
	private AddressRange dyn_area = null;
	
	public CpuExecutionAnalyzer(MemoryBlockInterface memory, AddressRange code_range, PrintStream log)
	{
		this.memory = memory;
		this.code_range = code_range;
		this.log = log;
		mem_defines = new int [memory.getData().length];
		Arrays.fill(mem_defines, -1);
	}

	@Override
	public void notifyPreExecute(CPU cpu) throws EmulatorException {
		int pc = cpu.getPC();
		if (code_range.contains(pc) || (dyn_area != null && dyn_area.contains(pc)))
		{
			Instruction instruction = assembler.getInstruction(memory.getData(), pc);
			boolean is_jump = instruction.getOpcode().equals("JMP");
			if (is_jump && instruction.getOperandValue() == pc)
			{
				int nmi_vec = getMemWord(0xFFFA);
				int irq_vec = getMemWord(0xFFFE);
				
				log.println("Endless Loop @$"+new HwWord(pc)+" waiting for: NMI@$"+new HwWord(nmi_vec)+" IRQ@$"+new HwWord(irq_vec));
				allswell = false;
				throw new BreakpointException(pc);
			}
			boolean is_indirect = instruction.isIndirectOperand();
			if (instruction.isAddressOperand())
			{
				AddressDefinition adrdef = new AddressDefinition();
				adrdef.setFullAddress(instruction.getOperandValue());
				adrdef.setInstruction(instruction);
				adrdef.setType(instruction.getByteCount() > 2 ? AddressDefinition.TYPE_ADR : AddressDefinition.TYPE_ZP);
				adrdef.setInstructionAddress(pc);
				if (!address_map.containsKey(pc+1))
					address_map.put(pc+1, adrdef);
				if (!is_indirect)
					is_indirect = (mem_defines[pc+1] >= 0) || ((instruction.getByteCount() > 2) && (mem_defines[pc+2] >= 0));
			}
			if (is_indirect)
			{
				int target = (instruction.isIndirectOperand() ? instruction.getOperandValue() : pc+1);
				boolean is_word_address = instruction.isIndirectOperand() || instruction.getByteCount() > 2;
				int ptr_val = getMemByte(target);
				if (is_word_address)
					ptr_val |= getMemByte(target+1) << 8;
				int def_lo = (is_jump ? target : mem_defines[target]);
				if (def_lo >= 0)
				{
					AddressDefinition adrdef_lo = new AddressDefinition();
					adrdef_lo.setFullAddress(ptr_val);
					adrdef_lo.setInstruction(instruction);
					adrdef_lo.setType(AddressDefinition.TYPE_LOW);
					adrdef_lo.setInstructionAddress(pc);
					address_map.put(def_lo, adrdef_lo);
				}
				if (is_word_address)
				{
					int def_hi = (is_jump ? target+1 : mem_defines[target+1]);
					if (def_hi >= 0)
					{
						AddressDefinition adrdef_hi = new AddressDefinition();
						adrdef_hi.setFullAddress(ptr_val);
						adrdef_hi.setInstruction(instruction);
						adrdef_hi.setType(AddressDefinition.TYPE_HIGH);
						adrdef_hi.setInstructionAddress(pc);
						address_map.put(def_hi, adrdef_hi);
					}
				}
			}
			if (instruction.isLoad())
			{
				int source_loc = instruction.isImmediate() ? pc + 1 : instruction.getOperandAddress(cpu);
				if (source_loc >= 0 && mem_defines[source_loc] >= 0)
					reg_defines.put(instruction.getStoreLoadReg(), mem_defines[source_loc]);
				else
					reg_defines.put(instruction.getStoreLoadReg(), source_loc);
			}
			else if (instruction.isStore())
				mem_defines[instruction.getOperandAddress(cpu)] = reg_defines.get(instruction.getStoreLoadReg());
			else if (instruction.isTransfer())
				reg_defines.put(instruction.getTargetReg(), reg_defines.get(instruction.getSourceReg()));
/*			Tracking Push/Pull only without regard to other stack operations (JSR/RTS/RTI/PHP/PLP/IRQ/NMI) is not useful! 
 			else if (instruction.getOpcode().equals("PLA"))
			{
				reg_defines.put('A', mem_defines[(int) (((Cpu6502)cpu).getSP().getNumber()+257)]);
				reg_defines.put('S', -1);
			}
			else if (instruction.getOpcode().equals("PHA"))
			{
				mem_defines[(int) (((Cpu6502)cpu).getSP().getNumber()+256)] = reg_defines.get('A');
				reg_defines.put('S', -1);
			} */
			else if (instruction.isCompare())
			{
				int operand_address = instruction.getOperandAddress(cpu);
				int reg_source = reg_defines.get(instruction.getCompareReg());
				CmpCommand cmp = new CmpCommand(pc, instruction, operand_address, reg_source, getMemByte(operand_address));
				cmp_map.add(cmp);
			}
		}
		else
		{
			allswell = false;
			throw new BusWatchException("PC out of range, last valid was "+new HwWord(last_pc), new HwWord(pc));
		}
		last_pc  = pc;
	}

	private int getMemWord(int address) {
		return getMemByte(address) + (getMemByte(address+1) << 8);
	}

	private int getMemByte(int address) {
		return ByteHelper.byteToInt(memory.getData()[address]);
	}

	@Override
	public void notifyPostExecute(CPU cpu) {
		// TODO: check premises
	}

	public void dumpStats(PrintStream out, String prefix) {
		out.println(prefix+"Relocations:");
		List<Integer> addresses = new ArrayList<Integer>(address_map.keySet());
		Collections.sort(addresses);
		for (int address : addresses)
		{
			if (!code_range.contains(address))
				address_map.get(address).print(out, prefix+"DYN $"+new HwWord(address)+": ");
			else
				address_map.get(address).print(out, prefix+"$"+new HwWord(address)+": ");
			out.println();
		}
	}

	public HashMap<Integer, AddressDefinition> getAddressMap() {
		return address_map;
	}

	public void initRun() {
		reg_defines.put('A', -1);
		reg_defines.put('X', -1);
		reg_defines.put('Y', -1);
		reg_defines.put('S', -1);
		allswell = true;
		last_pc = -1;
	}

	public boolean isOk() {
		return allswell;
	}

	public void setDynArea(AddressRange dyn_area) {
		this.dyn_area = dyn_area;
	}

	public int [] getAndClearDynAreaFlow()
	{
		int [] dyn_defines = new int [mem_defines.length];
		Arrays.fill(dyn_defines, -1);
		for (int i = dyn_area.getStart(); i <= dyn_area.getEnd(); i++)
		{
			dyn_defines[i] = mem_defines[i];
			mem_defines[i] = -1;
		}
		return dyn_defines;
	}

	public AddressRange getCodeRange() {
		return code_range;
	}

	public CmpCommandMap getCmpCommandMap() {
		return cmp_map;
	}
}
